﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Linq.Expressions;
using Fuse8.DomainFramework.Common;
using Fuse8.DomainFramework.Domain;

namespace Fuse8.DomainFramework.DataAccess.SqlDatabase
{
	internal class ExpressionToSqlTransformer
	{
		#region private fields
		/// <summary>
		/// Rules service instance
		/// </summary>
		private readonly IExpressionToSqlTransformRulesService _rulesService;
		
		/// <summary>
		/// Entity property mapping service
		/// </summary>
		private readonly IEntityToSqlMappingService _mappingService;
		
		/// <summary>
		/// Parameter for access to expression property
		/// </summary>
		private readonly ParameterExpression _parameter;

		private readonly string _defaultAlias;

		#endregion

		/// <summary>
		/// Constructor
		/// </summary>
		/// <param name="rulesService">Rules service instance</param>
		/// <param name="mappingService">Entity property mapping service</param>
		/// <param name="defaultAlias">Default alias for where entity</param>
		public ExpressionToSqlTransformer(IExpressionToSqlTransformRulesService rulesService, IEntityToSqlMappingService mappingService, string defaultAlias)
		{
			_rulesService = rulesService;
			_mappingService = mappingService;
			_parameter = Expression.Parameter(typeof(object), "p");
			_defaultAlias = defaultAlias;
		}

		/// <summary>
		/// Creates where clause from expression
		/// </summary>
		/// <typeparam name="T">Entity type</typeparam>
		/// <param name="expression">Expression</param>
		/// <returns>SqlQueryInfo with query text and query parameters delegates</returns>
		public SqlQueryInfo GetWhereSqlFromExpression<T>(Expression<Func<T, bool>> expression)
		{
			Expression acessorExpression = Expression.Property(Expression.Convert(_parameter, typeof(LambdaExpression)), "Body");
			var sqlParameters = new List<SqlParameterInfo>();
			var formattersStack = new Stack<IFormattingService>();
			var relatedProperties = new RelatedPropertyCollection();
			var query = VisitExpression<T>(expression.Body, acessorExpression, sqlParameters, formattersStack, relatedProperties, null);			
			return new SqlQueryInfo { Query = query, Parameters = sqlParameters, RelatedProperties = relatedProperties };
		}

		private string VisitExpression<T>(Expression expression, Expression acessorExpression, IList<SqlParameterInfo> sqlParameters, Stack<IFormattingService> formattersStack, RelatedPropertyCollection relatedProperties, ExpressionType? parentExpressionType)
		{
			var template = _rulesService.GetExpressionTemplate(expression);
			string result;
			if (expression is UnaryExpression)
			{
				result = VisitUnaryExpression<T>(template, (UnaryExpression)expression, acessorExpression, sqlParameters, formattersStack, relatedProperties, parentExpressionType);
			}
			else if (expression is BinaryExpression)
			{
				result = VisitBinaryExpression<T>(template, (BinaryExpression)expression, acessorExpression, sqlParameters, formattersStack, relatedProperties, parentExpressionType);
			}
			else if (expression is ConstantExpression)
			{
				result = VisitConstantExpression(template, (ConstantExpression)expression, formattersStack, parentExpressionType);
			}
			else if (expression is MemberExpression)
			{
				result = VisitMemberExpression<T>((MemberExpression)expression, acessorExpression, sqlParameters, formattersStack, relatedProperties, parentExpressionType);
			}
			else if (expression is MethodCallExpression)
			{
				result = VisitMethodCallExpression<T>((MethodCallExpression)expression, acessorExpression, sqlParameters, formattersStack, relatedProperties, parentExpressionType);
			}
			else if (expression is NewArrayExpression)
			{
				result = VisitNewArrayExpression<T>((NewArrayExpression)expression, acessorExpression, sqlParameters, formattersStack, relatedProperties, parentExpressionType);
			}
			else if (expression is InvocationExpression)
			{
				result = VisitInvocationExpression<T>((InvocationExpression)expression, acessorExpression, sqlParameters, formattersStack, relatedProperties, parentExpressionType);
			}
			else
			{
				throw new NotSupportedException(string.Format("Expression \"{0}\" is'n supported", expression));
			}

			var formatter = _rulesService.GetExpressionFormatter(expression);
			return formatter != null ? formatter.FormatObject(result, parentExpressionType).ToString() : result;
		}

		private string VisitInvocationExpression<T>(InvocationExpression expression, Expression acessorExpression, IList<SqlParameterInfo> sqlParameters, Stack<IFormattingService> formattersStack, RelatedPropertyCollection relatedProperties, ExpressionType? parentExpressionType)
		{
			Expression expr = Expression.Convert(acessorExpression, typeof(InvocationExpression));
			expr = Expression.Property(expr, "Expression");
			expr = Expression.Convert(expr, typeof (LambdaExpression));
			expr = Expression.Property(expr, "Body");
			return VisitExpression<T>(((LambdaExpression)(expression.Expression)).Body, expr, sqlParameters, formattersStack, relatedProperties, parentExpressionType);
		}

		private string VisitUnaryExpression<T>(TemplateResult template, UnaryExpression expression, Expression acessorExpression, IList<SqlParameterInfo> sqlParameters, Stack<IFormattingService> formattersStack, RelatedPropertyCollection relatedProperties, ExpressionType? parentExpressionType)
		{
			Expression expr = Expression.Convert(acessorExpression, typeof(UnaryExpression));
			expr = Expression.Property(expr, "Operand");
			var stack = AddFormatter(template.FormattingService, formattersStack);
			return template.Template.Replace("{operand}", VisitExpression<T>(expression.Operand, expr, sqlParameters, stack, relatedProperties, expression.NodeType));
		}

		private string VisitBinaryExpression<T>(TemplateResult template, BinaryExpression expression, Expression acessorExpression, IList<SqlParameterInfo> sqlParameters, Stack<IFormattingService> formattersStack, RelatedPropertyCollection relatedProperties, ExpressionType? parentExpressionType)
		{
			Expression expr = Expression.Convert(acessorExpression, typeof(BinaryExpression));
			var exprLeft = Expression.Property(expr, "Left");
			var exprRight = Expression.Property(expr, "Right");
			var stack = AddFormatter(template.FormattingService, formattersStack);
			var left = VisitExpression<T>(expression.Left, exprLeft, sqlParameters, stack, relatedProperties, expression.NodeType);
			var right = VisitExpression<T>(expression.Right, exprRight, sqlParameters, stack, relatedProperties, expression.NodeType);
			var result = template.Template.Replace("{left}", left).Replace("{right}", right);
			if (template.FormattingService != null)
			{
				result = template.FormattingService.FormatObject(result, parentExpressionType).ToString();
			}
			return result;
		}

		private string VisitConstantExpression(TemplateResult template, ConstantExpression expression, Stack<IFormattingService> formattersStack, ExpressionType? parentExpressionType)
		{			
			if (expression.Value == null)
			{
				return "NULL";
			}
			var stack = AddFormatter(template.FormattingService, formattersStack);
			return template.Template.Replace("{value}", ApplyFormattersToObject(expression.Value, expression.Type, stack, parentExpressionType).ToString());
		}

		private string VisitNewArrayExpression<T>(NewArrayExpression expression, Expression acessorExpression, IList<SqlParameterInfo> sqlParameters, Stack<IFormattingService> formattersStack, RelatedPropertyCollection relatedProperties, ExpressionType? parentExpressionType)
		{
			Expression expr = Expression.Convert(acessorExpression, typeof(NewArrayExpression));
			expr = Expression.Property(expr, "Expressions");
			expr = Expression.Convert(expr, typeof(ReadOnlyCollection<Expression>));
			expr = Expression.Property(expr, "Items");
			expr = Expression.Convert(expr, typeof(Expression[]));
			var result = new List<string>();
			for (var i = 0; i < expression.Expressions.Count; i++)
			{
				var exprIndex = Expression.Constant(i);
				var exprArg = Expression.ArrayIndex(expr, exprIndex);
				result.Add(VisitExpression<T>(expression.Expressions[i], exprArg, sqlParameters, formattersStack, relatedProperties, expression.NodeType));
			}
			return string.Join(", ", result.ToArray());
		}

		private string VisitMemberExpression<T>(MemberExpression expression, Expression acessorExpression, IList<SqlParameterInfo> sqlParameters, Stack<IFormattingService> formattersStack, RelatedPropertyCollection relatedProperties, ExpressionType? parentExpressionType)
		{
			if (expression.HasParameter())
			{
				var template = _rulesService.GetMemberTemplate(expression.Member);
				if (!string.IsNullOrEmpty(template))
				{
					var val = VisitExpression<T>(expression.Expression, acessorExpression, sqlParameters, formattersStack, relatedProperties, expression.NodeType);
					return template.Replace("{object}", val);
				}
				var property = AliasBuilder.BuildRelatedProperty(expression.Expression);
				var alias = AliasBuilder.BuildAlias(expression.Expression, _defaultAlias);
				if (property != null)
				{
					relatedProperties.Add(property);
				}

				var result = _mappingService.GetSqlNameByEntityName(expression.Expression.Type, expression.Member.Name, alias);
				return result;
			}

			var parameterName = GetParameterName(expression);
			if (!sqlParameters.Any(p => p.Name == parameterName))
			{
				sqlParameters.Add(new SqlParameterInfo { Name = parameterName, GetterDelegate = BuildGetterDelegate(expression, acessorExpression, _parameter, formattersStack, parentExpressionType) });
			}
			return GetParameterName(expression);
		}

		private string VisitMethodCallExpression<T>(MethodCallExpression expression, Expression acessorExpression, IList<SqlParameterInfo> sqlParameters, Stack<IFormattingService> formattersStack, RelatedPropertyCollection relatedProperties, ExpressionType? parentExpressionType)
		{
			var template = _rulesService.GetMethodTemplate(expression.Method);
			if (template == null)
			{
				return "";
			}
			Expression expr = Expression.Convert(acessorExpression, typeof(MethodCallExpression));
			var result = template.Template;

			if (expression.Object != null)
			{
				var exprObject = Expression.Property(expr, "Object");
				var val = VisitExpression<T>(expression.Object, exprObject, sqlParameters, formattersStack, relatedProperties, expression.NodeType);
				result = result.Replace("{object}", val);
			}

			expr = Expression.Property(expr, "Arguments");
			expr = Expression.Convert(expr, typeof(ReadOnlyCollection<Expression>));
			expr = Expression.Property(expr, "Items");
			expr = Expression.Convert(expr, typeof(Expression[]));

			for (var i = 0; i < expression.Arguments.Count; i++)
			{
				var exprIndex = Expression.Constant(i);
				var exprArg = Expression.ArrayIndex(expr, exprIndex);
				var stack = AddFormatter(template.FormattingService, formattersStack);
				result = result.Replace(string.Format("{{parameters[{0}]}}", i), VisitExpression<T>(expression.Arguments[i], exprArg, sqlParameters, stack, relatedProperties, expression.NodeType));
			}
			return result;
		}

		private static Func<object, object> BuildGetterDelegate(MemberExpression memberExpression, Expression acessorExpression, ParameterExpression parameter, Stack<IFormattingService> formattersStack, ExpressionType? parentExpressionType)
		{
			if (memberExpression == null) throw new ArgumentNullException("memberExpression");
			var memberItems = new List<MemberItem>();
			Expression expression = memberExpression;
			var ex = acessorExpression;

			while (!(expression is ConstantExpression))
			{
				memberItems.Add(new MemberItem { Member = ((MemberExpression)expression).Member, MemberType = ((MemberExpression)expression).Member.DeclaringType });
				ex = Expression.Convert(ex, typeof(MemberExpression));
				ex = Expression.Property(ex, "Expression");
				expression = ((MemberExpression)expression).Expression;
			}

			ex = Expression.Convert(ex, typeof(ConstantExpression));
			ex = Expression.Property(ex, "Value");

			memberItems.Reverse();
			foreach (var memberItem in memberItems)
			{
				ex = Expression.Convert(ex, memberItem.MemberType);
				ex = Expression.MakeMemberAccess(ex, memberItem.Member);
			}

			ex = ApplyFormattersToDelegate(Expression.Convert(ex, typeof(object)), formattersStack, parentExpressionType);
			return Expression.Lambda<Func<object, object>>(ex, parameter).Compile();
		}

		private static string GetParameterName(Expression expression)
		{
			var result = "";
			while (expression is MemberExpression)
			{
				result = ((MemberExpression)expression).Member.Name + result;
				expression = ((MemberExpression)expression).Expression;
			}

			return "@" + result;
		}

		private static Expression ApplyFormattersToDelegate(Expression expression, Stack<IFormattingService> formattersStack, ExpressionType? parentExpressionType)
		{
			var formatters = new Stack<IFormattingService>(formattersStack.ToArray().Reverse());
			var expr = expression;
			while (formatters.Any())
			{
				var formatter = formatters.Pop();
				expr = formatter.CreateFormatDelegate(expr, parentExpressionType);
			}
			return expr;
		}
		
		private static object ApplyFormattersToObject (object obj, Type expressionType, Stack<IFormattingService> formattersStack, ExpressionType? parentExpressionType)
		{
			var result = obj;
			var formatters = new Stack<IFormattingService>(formattersStack.ToArray().Reverse());			
			while (formatters.Any())
			{
				var formatter = formatters.Pop();
				result = formatter.FormatObject(result, parentExpressionType);
			}			
			return result;
		}

		private static Stack<IFormattingService> AddFormatter(IFormattingService formatter, Stack<IFormattingService> formattersStack)
		{
			if (formatter != null)
			{
				var formatters = new Stack<IFormattingService>(formattersStack.ToArray().Reverse());
				formatters.Push(formatter);
				return formatters;
			}
			return formattersStack;
		}
	}
}